package novel.manager.service.impl.spider.download;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import novel.manager.pojo.spider.Chapter;
import novel.manager.pojo.spider.ChapterDetail;
import novel.manager.pojo.spider.ThreadConfiguration;
import novel.manager.service.iface.spider.chapter.IChapterDetailSpider;
import novel.manager.service.iface.spider.chapter.IChapterSpider;
import novel.manager.service.iface.spider.download.INovelDownload;
import novel.manager.service.utils.ChapterDetailSpiderFactory;
import novel.manager.service.utils.ChapterSpiderFactory;
import novel.manager.service.utils.NovelSpiderUtil;

/**
 * 如何实现多线程下载任意网站的小说
	1.拿到该网站的某本小说的所有章节：章节列表
	2.通过计算，将这些章节分配给指定数量的线程，让他们去解析，然后保存到文本文件中
	3.通知主线程，将这些零散的小文件合并成一个大文件。最后将那些分片的小文件删除掉。
 */
public class NovelDownload implements INovelDownload {
	@Override
	//url：章节列表
	public String download(String url, ThreadConfiguration threadConfiguration) {
		//通过给定的url，返回一个实现了IChapterSpider接口的实现类DefaultChapterSpider或BxwxChapterSpider
		IChapterSpider spider = ChapterSpiderFactory.getChapterSpider(url);
		//抓取任意网站的章节列表
		List<Chapter> chapters = spider.getChapters(url);
		//某个线程下载完毕之后，你得告诉主线程：我下载好了
		//所有的线程都下载好了，合并！！！
		int size = threadConfiguration.getSize();
		// 2010章 100章
		// 需要21个线程
		// 一个int / int 结果只能是int
		// 一个double / double 结果依然是double
		//一个double / int 结果是double
		//Math.ceil(double) 10 -> 10 10.5->11 10.1 ->11 -10 -> -10 -10.1 -> 10 -10.5 -> -10
		int maxThreadSize = (int)Math.ceil(chapters.size() * 1.0 / size);
		//每个线程下载的章节子列表
		Map<String, List<Chapter>> downloadTaskAlloc = new HashMap<>();
		for (int i = 0; i < maxThreadSize; i++) {
			// i = 0 0-99	-- > 100  0 100
			// i = 1 100-199
			// i = 2 200-299 
			// i = 3 300-399 
			// ...
			// i = 19 1900-1999
			// i = 20 2000-2052
			// 总共才2053章
			int fromIndex = i * threadConfiguration.getSize();
			int toIndex = i == maxThreadSize - 1 ? chapters.size() : i * threadConfiguration.getSize() + threadConfiguration.getSize();
			//划分出每个线程下载的章节子列表
			//subList：前闭后开
			downloadTaskAlloc.put(fromIndex + "-" + toIndex, chapters.subList(fromIndex, toIndex));
		}
		/**Java通过Executors提供四种线程池，分别为：
		newCachedThreadPool创建一个可缓存线程池，如果线程池长度超过处理需要，可灵活回收空闲线程，若无可回收，则新建线程。
		newFixedThreadPool 创建一个定长线程池，可控制线程最大并发数，超出的线程会在队列中等待。
		newScheduledThreadPool 创建一个定长线程池，支持定时及周期性任务执行。
		newSingleThreadExecutor 创建一个单线程化的线程池，它只会用唯一的工作线程来执行任务，保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。*/
		ExecutorService service = Executors.newFixedThreadPool(maxThreadSize);		
		//通过这两段代码就可以创建缺失的路径，添加一个上层目录
		//String savePath = config.getPath() + "/" + NovelSiteEnum.getEnumByUrl(url).getUrl();
		String savePath = threadConfiguration.getPath();
		//实例化目录：Creates the directory named by this abstract pathname, including any necessary but nonexistent parent directories. 
		new File(savePath).mkdirs();
		//章节文件子列表的key集合：线程任务的分配
		Set<String> keySet = downloadTaskAlloc.keySet();
		//期货订单集合
		List<Future<String>> futures = new ArrayList<>();
		String path=null;	
		//遍历线程任务的分配并下单
		for (String key : keySet) {	
			path=savePath + "/" + key + ".txt";
			List<Chapter> chapters2= downloadTaskAlloc.get(key);
			int tryTimes=threadConfiguration.getTryTimes();
			//提交任务给线程池执行任务:期货下单操作，并添加到期货订单集合
			futures.add(service.submit(new DownloadCallable(path,chapters2,tryTimes )));
		}
		// 关闭线程池  
		/*当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态,以后不能再往线程池中添加任何任务，否则将会抛出RejectedExecutionException异常。
		 * 但是，此时线程池不会立刻退出，直到添加到线程池中的任务都已经处理完成，才会退出。
		 *  与它相似的还有一个shutdownNow()，它通过调用Thread.interrupt来实现线程的立即退出。*/
		service.shutdown();
		//遍历期货订单集合进行交割提货，打印线程的任务完成的标志
		for (Future<String> future : futures) {
			try {
				//future.get()：Waits if necessary for the computation to complete, and then retrieves its result.
				//等待线程任务完成，打印完成下载的文件路径和线程任务完成的标志
				/*回车符：/r Mac；/n Unix/Linux；/r/n Windows     */
				System.out.println("path:"+path+"/r/n  url:"+url+"/r/n"+future.get() + ",下载完成！");
			} catch (InterruptedException | ExecutionException e) {
				e.printStackTrace();
			}
		}
		//合并子文件
		NovelSpiderUtil.multiFileMerge(savePath, null, true);
		return savePath + "/merge.txt";
	}
}
//子线程任务：下单计划
class DownloadCallable implements Callable<String> {
	private List<Chapter> chapters;
	private String path;
	private int tryTimes;
	public DownloadCallable(String path, List<Chapter> chapters, int tryTimes) {
		this.path = path;
		this.chapters = chapters;
		this.tryTimes = tryTimes;
	}
	@Override
	public String call() throws Exception {
		System.out.println("DownloadCallable.call()");
		try (
				//字符打印流：
				//file:The file to use as the destination of this writer.输出的目的地
				PrintWriter out = new PrintWriter(new File(path), "UTF-8");
			) {
			for (Chapter chapter : chapters) {
				//使用工厂模式，用接口代理返回章节内容实现类
				IChapterDetailSpider spider = ChapterDetailSpiderFactory.getChapterDetailSpider(chapter.getUrl());
				ChapterDetail detail = null;
				String url=null;
				String title=null;
				String content=null;
				for (int i = 0; i < tryTimes; i++) {
					try {
						//获取章节内容
						url=chapter.getUrl();
						System.out.println("url:"+url);
						//抓取只能归结章节内容
						detail = spider.getChapterDetail(url);
						System.out.println("detail:"+detail);
						title=detail.getTitle();
						System.out.println("title:"+title);
						content=detail.getContent();
						System.out.println("content:"+content);
						//选择需要的章节标题和内容进行下载
						out.println(title);
						out.println(content);
						break;
					} catch (RuntimeException e) {
						/**java try catch中如何抛出异常 让程序停止运行？？
						 * 真要停止的话用System.exit(1)更合适。
						 * 在一个if-else判断中，如果我们程序是按照我们预想的执行，到最后我们需要停止程序，
						 * 那么我们使用System.exit(0)，而System.exit(1)一般放在catch块中，当捕获到异常，需要停止程序，
						 * 我们使用System.exit(1)。这个status=1是用来表示这个程序是非正常退出。
							一般还是throw RuntimeException更合适一点，调用方可以决定要不要停止。
							不catch就可以了， 程序会退出， 俗称闪退。
						 * */
						//检查发现有些章节确实下不了Parse error: parse error in E:\kanshuzhong\wwwroot\html\1\1236\13432751.html on line 21
						System.err.println("title:"+title+"/n  url:"+url+" /n  尝试第[" + (i + 1) + "/" + tryTimes + "]次下载失败了！");						
						//System.exit(1);
					}
				}
			}
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		//返回任意String:线程的任务完成的标志:返回完成下载的文件路径
		return path;
	}
	
}